package bsserver;

import bsserver.response.ErrorResponse;
import bsserver.response.Response;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Vector;

/**
 * Class which encapsulates all information about an incoming Request
 * @author tblackstone
 */
public class Request {

    private String method;
    private RequestURI requestURI;
    private String version;
    private HashMap<String, String> headers;
    private byte[] messageBody;
    private BufferedInputStream in;
    private Response response = null;
    private String requestLine;
    private String remoteHost;
    private String remoteUser = "";
    private Auth userAuthInfo;

    /**
     * Returns string up to but not including /r/n.  Reutrns null
     * if first two bytes encountered are /r/n.
     * @param input input stream from client
     * @return string containing line
     * @throws java.io.IOException
     */
    private String readLine(BufferedInputStream input) throws IOException{
        String myLine = null;
        Vector<Byte> myBytes = new Vector<Byte>();
        byte b = (byte) 0;


        //while( input.available() > 0) {
        //    b = (byte) input.read();
        while ((b = (byte)input.read()) != -1) {


            if( b == '\r'){
                // *must* be a newline
                b = (byte) input.read();
                break;
            }

            if(myLine == null){
                myLine = new String();
            }


            myBytes.add(b);
        }


        // have to do strange conversions from Bytes to bytes to string
        // to not introduce artifacts for binary data
        if(myBytes.size() > 0){
            byte[] normByteArray = new byte[myBytes.size()];


            for(int i=0;i<myBytes.size();i++){

                normByteArray[i] = myBytes.get(i).byteValue();
            }

            myLine = new String(normByteArray);
        }


        return myLine;
    }


    /**
     * Create a new Request object based on the given request input
     * @param remoteHost the remote host's Internet address
     * @param input the buffered input stream to read this request from
     */
    public Request(String remoteHost, BufferedInputStream input) {
        this.remoteHost = remoteHost;
        this.in = input;
        headers = new HashMap<String, String>();
    }

    private void parseRequestLine() throws IOException {
        requestLine = readLine(in);
        if (requestLine == null) {
            response = new ErrorResponse(this, 400);
            return;
        }

        // Request line must be in the following format:
        // method SP http_uri SP version

        System.out.println("*** Request: " + requestLine);
        String reqs[] = requestLine.split(" ");
        if (reqs.length != 3) {
            response = new ErrorResponse(this, 400);
            return;
        }

        method = reqs[0];

        try {
            requestURI = new RequestURI(reqs[1]);
        } catch (URISyntaxException ex) {
            response = new ErrorResponse(this, 400);
        }

        version = reqs[2];
    }

    /**
     * Parses all request headers, separating the field and value pairs.
     * @throws IOException
     */
    private void parseHeaders() throws IOException {

        String headerLine;

        while ((headerLine = readLine(in)) != null) {
            if (headerLine.equals("")) {
                break;
            }

            // Split header only on first semicolon
            String vals[] = headerLine.split(":", 2);
            if (vals.length != 2) {
                response = new ErrorResponse(this, 400);
                return;
            }
            setHeader(vals[0].trim(), vals[1].trim());

        }

    }

    private void parseBody() throws IOException {

        /* the input stream has been queued to the point
         * of the body.  We've paresed out headers and
         * request line.
         */
        String body = new String();
        if (headers.containsKey("Content-Length")){
            int bodyLen = Integer.parseInt(headers.get("Content-Length"));

            messageBody = new byte[bodyLen];

            int actualLength = in.read(messageBody);

            headers.put("Content-Length", Integer.toString(actualLength));

     //       if( actualLength != bodyLen)
     //           response = new ErrorResponse(this,400);

        }
    }

    /**
     * Saves a request header field/value pair received from the client.  Any
     * existing header with the same field name is replaced.
     * @param header the request header field to store
     * @param value the value of the request header field
     */
    private void setHeader(String header, String value) {
        headers.put(header, value);
    }

    /**
     * Saves the body of the HTTP request.
     * @param body A String containing the entire message body.
     */
    private void setBody(String body) {
        try {
            messageBody = body.getBytes("UTF-8");
        } catch (UnsupportedEncodingException ex) {
            messageBody = body.getBytes();
        }

    }

    /**
     * Saves the body of the HTTP request.
     * @param body A byte array containing the entire message body.
     */
    private void setBody(byte[] body) {
        messageBody = body;
    }

    /**
     * Returns the HTTP method specified in the Request
     * @return a String containing the HTTP method specified in the Request
     */
    public String getMethod() {
        return method;
    }

    /**
     * Returns the requested resource URI
     * @return the URI specified in the Request
     */
    public RequestURI getRequestURI() {
        return requestURI;
    }

    /**
     * Returns the HTTP version used in the Request
     * @return the HTTP version
     */
    public String getHTTPVersion() {
        return version;
    }

    /**
     * Gets the value associated with a request header field
     * @param header
     * @return the value of the given request header field, or <tt>null</tt> if
     *      the field has not been set
     */
    public String getHeader(String header) {
        return headers.get(header);
    }

    /**
     * Returns a HashMap of all the headers submitted in the Request
     * @return a HashMap of header names mapped to their values
     */
    public HashMap<String, String> getAllHeaders() {
        return headers;
    }

    /**
     * Returns the body of the HTTP request.
     * @return A byte array containing the entire message body.
     */
    public byte[] getBody() {
        return messageBody;
    }

    /**
     * Parses this Request and creates a Response object.
     * @return the Response object created.
     * @throws IOException
     */
    public Response getResponse() throws IOException {


        parseRequestLine();
        if (response == null) {
            parseHeaders();
        }
        if (response == null) {
            parseBody();
        }

        if (response != null) {
            return response;
        }

        return Response.generate(this);

    }

    /**
     * Gets the entire Request line sent by the client
     * @return the Request line, as specified in RFC 2616 section 5.1
     */
    public String getRequestLine() {
        return requestLine;
    }

    /**
     * Returns the Internet address of the remote host who sent the Request
     * @return the remote host's Internet address
     */
    public String getRemoteHost() {
        return remoteHost;
    }

    /**
     * Returns the authorized remote host's user name.
     * @return The user's name if provided, otherwise the empty string "".
     */
    public String getRemoteUser() {
        return remoteUser;
    }

    /**
     * Sets the remote host's user name.
     * @param user the name which the remote host has authenticated with
     */
    public void setRemoteUser(String user) {
        remoteUser = user;
    }

    /**
     * Returns an Auth object which contains information about the remote host's
     * permissions with respect to the requested resource.
     * @return the Auth object associated with the requested resource
     */
    public Auth getAuthInfo() {
        return userAuthInfo;
    }

    /**
     * Sets this request's Auth object
     * @param authInfo the new Auth object to associate this request with
     */
    public void setAuthInfo(Auth authInfo) {
        userAuthInfo = authInfo;
    }
}
